'''
- Are account numbers standardized (per industry)?

Type of accounts: balance sheet accounts
   - asset
   - liability
   - equity      
   - revenue
   - expense
 
- owners equity
   - revenues 
   - expenses
   - investment
   - draws
   
 
accounts are either a balance sheet or income statement account

example transaction:
    load from bank (increases cash and notes payable):
        debit cash   
        credit notes payable
        
TODO
- select time data type to use for transactions 
    ibm_db_dbi maps timestamp onto datetime
- enforce accounting rules


Done:
- Removed amount from Transaction: amounts only apply to affected accounts:
     tx = Transaction(datetime.now(), 100, "borrow 100", {cashAcct: 100, notesPayableAcct: 100})

'''
from datetime import datetime
import logging


LOG_FORMAT = '%(asctime)s %(levelname)-8s %(name)-8s %(message)s'

# asset/expense accounts
DEBIT_INCREMENTS = 1
# liability accounts
CREDIT_INCREMENTS = 2

DEBIT = 1
CREDIT = 2

DATE_FORMAT = "%Y-%m-%d"

def parseDate(s):
    return datetime.strptime(s, DATE_FORMAT)
    
class AccountType:
    def __init__(self, name, type): 
        self.name = name
        self.type = type


class AccountEntry:
    def __init__(self, date, amount, description):
        self.date = date
        self.amount = amount
        self.description = description

class Account:
    def __init__(self, nr, name, type):
        self.nr = nr
        self.name = name
        self.type = type
        self.entries = []

    def getChangeType(self, amount):
        """
        Returns credit or debit depending on the amount (pos/neg)
        and account type (debit vs credit increments)
        """
        if amount >= 0:
            if self.type == DEBIT_INCREMENTS:
                return DEBIT
            else:
                return CREDIT
        else:
            if self.type == DEBIT_INCREMENTS:
                return CREDIT
            else:
                return DEBIT
            
    def isDebit(self, amount):
        return self.getChangeType(amount) == DEBIT
      
    def isCredit(self, amount):
        return self.getChangeType(amount) == CREDIT
    
    def post(self, date, description, amount):
        
        self.entries.append(AccountEntry(date, amount, description))
        
    def getBalance(self):
        total= 0
        for entry in self.entries:
            total = total + entry.amount
        return total
            
        
    def print1(self):
        # TODO: investigate reportlab
        
        print "\nAccount: %s\n" % self.name
        print "%-8s %-30s %-9s %-9s" % ("Date", "Description of entry", "Debit", "Credit")
        print "-------- ------------------------------ --------- ---------"
        
        for entry in self.entries:
            if self.isDebit(entry.amount):
                print "%-8s %-30s %9.02f" % (entry.date.strftime("%m/%d"), 
                                               entry.description, abs(entry.amount))
            else:
                # credited account is indented
                print "%-8s %-30s %9s %9.02f" % (entry.date.strftime("%m/%d"), 
                                                     entry.description, "", abs(entry.amount))



# turns out that transactions do not involve amounts:
# the amounts are specific to the accounts the transaction affects
class Transaction:
    def __init__(self, date, description, accounts):
        self.date = date
        self.description = description
        self.accounts = accounts
        
        self.validate()
    
    def validate(self):
        debits = 0
        credits = 0
        
        for account, amount in self.accounts.iteritems():
            if account.isDebit(amount):
                debits += abs(amount)
            else:
                credits += abs(amount)
        
        if debits <> credits:
            raise Exception("total debits <%02.f> not equal to credits <%02.f>" % ( 
                            debits, credits))  
        
    def post(self):
        for account in self.accounts:
            account.post(self.date, self.description, self.accounts[account])

# TODO: add journal entry class

class Journal:
    
    def __init__(self, name):
        self.name = name
        self.txns = []
        
    def add(self, tx):
        self.txns.append(tx)
        
    def addTransaction(self, date, description, accounts):
    
        tx = Transaction(parseDate(date), description, 
                      accounts)
        self.add(tx)

    def post(self): 
        for txn in self.txns:
            txn.post()
                
    def print1(self):
        print "%-8s %-30s %-9s %-9s" % ("Date", "Description of entry", "Debit", "Credit")

        for tx in self.txns:
            self.printTx(tx)
            
    def printTx(self, tx):
        # get debit
        logger = logging.getLogger("printTx")
        
        
        # Write the debit transactions (follow rule to print debit first)
        debitAcct = None
        
        for acct in tx.accounts:
            amt = tx.accounts[acct]
                        
            if acct.isDebit(amt):
                debitAcct = acct        
                break
            
        if debitAcct is None:
            logger.error("no debit account found in transaction <%s>" % tx)
            return
        
        print "%-8s %-30s %10.02f" % (tx.date.strftime("%m/%d"), debitAcct.name, tx.accounts[debitAcct])
        
        # print the rest
        for acct in tx.accounts:
            if acct == debitAcct:
                continue
            
            amt = tx.accounts[acct]
            
            # determine debit vs credit
            # since we're printing increments/decrements in terms of debit/credit
            # take the amount in absolute terms
            if acct.isDebit(amt):
                print "%-8s %-30s %10.02f" % ("", acct.name, abs(tx.accounts[acct]))
            else:
                # credited account is indented
                print "%-8s     %-26s %10s %10.02f" % ("", acct.name, "", abs(tx.accounts[acct]))
                            

        # finally, print the description        
        print "%-8s %-30s %-10s %-10s" % ("", tx.description, "", "")
        

class Ledger:
    def __init__(self):
        self.accounts = []
        self.accountId = 0
    
    def chartOfAccounts(self):
        # p25: organize accounts by type (e.g. assets, liabilities etc)
        print "Chart Of Accounts"
        for account in self.accounts:
            print "%d: %s" % (account.nr, account.name)
            
    
    
    def addAccount(self, name, type):
        self.accountId += 1
        
        account = Account(self.accountId, name, type)
        self.accounts.append(account)
           
        return account 

    def trialBalance(self):
        # p26
        """
         process:
         for each account:
             get the balance
             add up debit balances
             add up credit balances
             total debit should equal credit 
             
        Note: since transactions check that credits == debits, this check is redundant 
        """
        debits = 0
        credits = 0
        
        print "\nTrail Balance\n"
        for account in self.accounts:
            
            balance = account.getBalance()                        
            print "%d: %-20s %s" % (account.nr, account.name, formatDebitCredit(account, balance))
            
            if account.isDebit(balance):
                debits += balance
            else:
                credits += balance
            # TODO: generalize printing of debits/credits

        print "============================================="
        print " : Total                %10.02f  %10.02f " % (debits, credits)
                                
# page 26
    
def formatDebitCredit(acct, amount):
    if acct.isDebit(amount):
        return "%10.02f" % amount
    else:
        # credited account is indented
        return "            %10.02f" % amount

    

def chap3journal():
    ledger = Ledger()
    
    cashAcct = ledger.addAccount("Cash", DEBIT_INCREMENTS)
    rentExpAcct = ledger.addAccount("Rent-Expense", DEBIT_INCREMENTS)
    suppliesAcct = ledger.addAccount("Supplies", DEBIT_INCREMENTS)
    inventoryAcct = ledger.addAccount("Inventory", DEBIT_INCREMENTS)
# owner equity?
    eqAcct = ledger.addAccount("J. Young, Capital", CREDIT_INCREMENTS) # liability, incremented when purchasing on account
    accountsPayableAcct = ledger.addAccount("Accounts Payable", CREDIT_INCREMENTS)
    revenueAcct = ledger.addAccount("Revenue", CREDIT_INCREMENTS)
# Chapter 3, Exercise 3.1
    journal = Journal("General Journal")
# coding rule: first list the debit accounts
    journal.addTransaction("2011-01-02", "Owner invests cash into business", {cashAcct:10000, eqAcct:10000})
# could be a payRent method?
    journal.addTransaction("2011-01-03", "Rent Expense", {rentExpAcct:1000, cashAcct:-1000})
    journal.addTransaction("2011-01-05", "Purchase office supplies", {suppliesAcct:50, cashAcct:-50})
    journal.addTransaction("2011-01-09", "Purchase inventory on account", {inventoryAcct:900, accountsPayableAcct:900})
    journal.addTransaction("2011-01-14", "Sales Revenue", {cashAcct:250, revenueAcct:250})
# TODO: may post immediately at add transaction time instead?
    journal.post()

    journal.print1()

def chap4trailBalance():

    ledger = Ledger()
    
    cashAcct = ledger.addAccount("Cash", DEBIT_INCREMENTS)
    adExpAcct = ledger.addAccount("Advertising Expense", DEBIT_INCREMENTS)
    wagesExpAcct = ledger.addAccount("Wages Expense", DEBIT_INCREMENTS)
    suppliesAcct = ledger.addAccount("Purchased Supplies", DEBIT_INCREMENTS)

    revenueAcct = ledger.addAccount("Revenue", CREDIT_INCREMENTS)
    accountsPayableAcct = ledger.addAccount("Accounts Payable", CREDIT_INCREMENTS)

    journal = Journal("General Journal")

    journal.addTransaction("2011-08-04", "Sales Revenue", {cashAcct:2800, revenueAcct:2800})
    journal.addTransaction("2011-08-07", "Purchase Supplies", {suppliesAcct:1550, accountsPayableAcct:1550})
    journal.addTransaction("2011-08-11", "Sales Revenue", {cashAcct:4100, revenueAcct:4100})
    journal.addTransaction("2011-08-14", "Ad In newspaper", {adExpAcct:1250, cashAcct:-1250})
    journal.addTransaction("2011-08-18", "Sales Revenue", {cashAcct:3200, revenueAcct:3200})
    journal.addTransaction("2011-08-25", "Sales Revenue", {cashAcct:3850, revenueAcct:3850})
    journal.addTransaction("2011-08-28", "Paid Supplier", {accountsPayableAcct:-1550, cashAcct:-1550})
    journal.addTransaction("2011-08-30", "Paid Wages", {wagesExpAcct:2600, cashAcct:-2600})

    journal.post()
    journal.print1()
    
    ledger.trialBalance()    

def main():
    logging.basicConfig(format=LOG_FORMAT, level=logging.DEBUG)
    logger = logging.getLogger("main")
    logger.debug("start")


    # chap3journal()
    
    chap4trailBalance()
    
    
    
    
    
main()    

